import {
  createContext,
  PropsWithChildren,
  ReactElement,
  ReactNode,
  useCallback,
  useContext,
  useLayoutEffect,
  useRef,
  useState,
} from "react";
import {
  useClick,
  useFloating,
  useDismiss,
  useInteractions,
  useListNavigation,
  UseInteractionsReturn,
  FloatingList,
  useRole,
  flip,
  offset,
  useListItem,
  size,
  autoUpdate,
  FloatingFocusManager,
  FloatingPortal,
} from "@floating-ui/react";
import { m, AnimatePresence } from "framer-motion";
import { useLatestRef } from "../hooks";
import { clsxm } from "../helpers";

type SelectContextOptions<T = any> = {
  getItemProps: UseInteractionsReturn["getItemProps"];
  currentValue: T;
  handleSelect: (value: T) => void;
  compare: (a: T, b: T) => boolean;
  activeIndex: number | null;
  open: boolean;
};
const SelectContext = createContext({} as SelectContextOptions);

export type SelectProps<T> = PropsWithChildren<{
  value: T;
  onChange?: (value: T) => void;
  onSelect?: (item: NonNullable<T>) => void;
  compare?: (a: NonNullable<T>, b: NonNullable<T>) => boolean;
  /** @deprecated */
  renderSelected?: (value: NonNullable<T>) => ReactNode;
  renderTrigger?: (value: T) => ReactNode;
  className?: string;
  contentClassName?: string;
  focusClassName?: string;
}>;

export function Select<T>(props: SelectProps<T>) {
  const {
    value,
    onChange,
    onSelect,
    children,
    compare = Object.is,
    renderTrigger = String,
    className,
    contentClassName,
    focusClassName,
  } = props;
  const [open, setOpen] = useState(false);
  const [focusLabel, setFocusLabel] = useState(false);
  const { floatingStyles, refs, context, placement } = useFloating({
    open,
    onOpenChange: setOpen,
    transform: false,
    whileElementsMounted: autoUpdate,
    middleware: [
      flip(),
      offset({ mainAxis: 10 }),
      size({
        apply({ rects, elements, availableHeight }) {
          Object.assign(elements.floating.style, {
            maxHeight: `${availableHeight}px`,
            minWidth: `${rects.reference.width}px`,
          });
        },
        padding: 10,
      }),
    ],
  });
  const [activeIndex, setActiveIndex] = useState<number | null>(null);
  const listRef = useRef<HTMLLIElement[]>([]);

  const dismiss = useDismiss(context);
  const click = useClick(context, { event: "click" });
  const role = useRole(context, { role: "select" });
  const list = useListNavigation(context, {
    activeIndex,
    onNavigate: setActiveIndex,
    listRef,
    loop: true,
  });

  const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions(
    [dismiss, click, role, list],
  );
  const _onSelect = useLatestRef(onSelect);
  const _onChange = useLatestRef(onChange);

  const handleSelect = useCallback((value: NonNullable<T>) => {
    (refs.domReference.current as HTMLDivElement)?.focus();
    _onSelect.current?.(value);
    _onChange.current?.(value);
    setOpen(false);
  }, []);

  useLayoutEffect(() => {
    if (open) {
      setFocusLabel(true);
    }
  }, [open]);

  return (
    <>
      <div
        tabIndex={0}
        className={clsxm(
          "inline-block cursor-pointer overflow-hidden text-ellipsis whitespace-nowrap rounded border border-transparent border-zinc-400 px-2 py-0.5 text-sm outline-none transition-all duration-200 focus:border-sky-500 focus:shadow-[0_0_0_3px] focus:shadow-sky-200",
          className,
          focusLabel && [
            "border-sky-500 shadow-[0_0_0_3px] shadow-sky-200",
            focusClassName,
          ],
        )}
        ref={refs.setReference}
        {...getReferenceProps({
          onClick: function () {
            (refs.domReference.current as HTMLDivElement)?.focus();
          },
        })}
      >
        {renderTrigger(value)}
      </div>
      <AnimatePresence onExitComplete={() => setFocusLabel(false)}>
        {open && (
          <FloatingPortal>
            <FloatingFocusManager context={context} initialFocus={-1}>
              <m.div
                ref={refs.setFloating}
                initial={{ opacity: 0, scale: 0.95 }}
                animate={{ opacity: 1, scale: 1 }}
                exit={{ opacity: 0, scale: 0.95 }}
                transition={{ duration: 0.1 }}
                className={clsxm(
                  "z-50 overflow-hidden rounded bg-white shadow-lg outline-none",
                  contentClassName,
                  placement === "bottom" && "origin-top",
                  placement === "top" && "origin-bottom",
                )}
                style={{
                  ...floatingStyles,
                  width: refs.reference.current?.getBoundingClientRect().width,
                }}
                {...getFloatingProps()}
              >
                <ul className="text-sm">
                  <FloatingList elementsRef={listRef}>
                    <SelectContext.Provider
                      value={{
                        getItemProps,
                        currentValue: value,
                        handleSelect,
                        compare,
                        activeIndex,
                        open,
                      }}
                    >
                      {children}
                    </SelectContext.Provider>
                  </FloatingList>
                </ul>
              </m.div>
            </FloatingFocusManager>
          </FloatingPortal>
        )}
      </AnimatePresence>
    </>
  );
}

export type SelectItemProps<T> = {
  label: string;
  value: T;
  className?: string;
  activeClassName?: string;
  selectedClassName?: string;
};
export function SelectItem<T>(props: SelectItemProps<T>) {
  const { label, value, className, activeClassName, selectedClassName } = props;
  const {
    getItemProps,
    handleSelect,
    compare,
    currentValue,
    activeIndex,
    open,
  } = useContext(SelectContext);
  const { ref, index } = useListItem();

  const isSelect =
    currentValue !== null &&
    typeof currentValue !== "undefined" &&
    compare(currentValue, value);

  return (
    <li
      ref={ref}
      tabIndex={open && activeIndex === index ? 0 : -1}
      title={label}
      className={clsxm(
        "cursor-pointer overflow-hidden text-ellipsis whitespace-nowrap px-2 py-1 outline-none hover:bg-blue-100",
        className,
        activeIndex === index && ["bg-blue-200", activeClassName],
        isSelect && ["bg-blue-300", selectedClassName],
      )}
      {...getItemProps({
        onClick: function () {
          handleSelect(value);
        },
        onKeyDown(e) {
          if (e.key === "Enter") {
            handleSelect(value);
          }
        },
      })}
    >
      {label}
    </li>
  );
}
